Skip to content

fix(deploy): make the Render blueprint deployable (verified against the image) - #63

Open
adamXbot wants to merge 2 commits into
mainfrom
fix/render-blueprint-deploy
Open

fix(deploy): make the Render blueprint deployable (verified against the image)#63
adamXbot wants to merge 2 commits into
mainfrom
fix/render-blueprint-deploy

Conversation

@adamXbot

@adamXbot adamXbot commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Pre-deploy verification of deploy/render.yaml.example for a both-in-Render (web service + Postgres) deployment. The blueprint could not have provisioned as written, and had two silent defects that matter specifically for a tripwire service. Every fix is verified against the production Docker image, not just the docs.

Blocking defects (blueprint would not provision)

  • plan: starter on the database — Render now classes starter/standard/pro as legacy Postgres instance types that cannot be used for new databases. → basic-256mb (current flexible type, ~$6/mo).
  • No database region — the DB region defaulted independently of the web service's oregon. Private-network connection strings only resolve within the same region, and since ipAllowList: [] disables public ingress, a region mismatch leaves the app with no route to Postgres at all. Both are now pinned together with a comment saying they must move as a pair.

Silent defects

  • Null client IPs — same bug as fixed for Fly in feat(deploy): one-command Fly.io launch + fix null client IPs on Fly #58: the image sets NODE_ENV=production, where mantis distrusts forwarding headers unless TRUST_PROXY_HEADERS=1. Without it every hit records ip = null. Also pins TRUSTED_IP_HEADER=x-forwarded-for since Render does not strip an inbound CF-Connecting-IP (forgeable with one request header otherwise). Comment covers the cf-connecting-ip switch if fronted by Cloudflare later.
  • PUBLIC_BASE_URL vs the global onrender.com namespace<name>.onrender.com is shared by all Render customers; plain mantis is taken, and when a name is taken Render silently serves at <name>-<suffix>.onrender.com. The hardcoded base URL would then mint canary URLs pointing at another customer's app — handed-out canaries would trigger someone else's server. Usage now: pick a unique name, verify the served URL after first deploy, fix it in this file (the blueprint is the source of truth; dashboard edits to synced vars are overwritten on sync).
  • 15 GB default disk — Basic-tier databases get 15 GB when diskSizeGB is omitted, ~3× the storage cost for nothing. Pinned to 5 GB (disks grow but never shrink).

Improvements

  • MANTIS_API_KEY_PEPPER uses generateValue: true — generated once at create, never rotated on later deploys, which is exactly the write-once property the pepper needs; the operator never handles the value. (Recreating the service from scratch requires copying it across first — documented.)
  • postgresMajorVersion: "18" — parity with docker-compose, CI, and the tier-2 suite, all on postgres:18.
  • Documented the first-boot admin-key flow, including the trap that a bare grep for mantis_live_ returns an 18-char prefix from a structured log line that 401s — take the key from the banner.
  • Optional blocks for BOOTSTRAP_API_KEY, retention (MANTIS_*_RETENTION_DAYS), and SMTP; sharper free-tier warning (a tripwire is idle by definition, so spin-down means nearly every real hit pays the cold start).

Verification

Booted the production image with exactly the blueprint's env against Postgres 18.4 (and previously 17.10):

  • migrations applied automatically; /api/health{"status":"ok","db":"ok"}
  • first-boot banner key authenticates: 200 with it, 401 without
  • trigger with X-Forwarded-For: 198.51.100.42 + forged CF-Connecting-IP: 6.6.6.6 records 198.51.100.42
  • YAML parses; regions match; fromDatabase reference resolves; diskSizeGB satisfies the 1-or-multiple-of-5 rule; version is a quoted string
  • 217/217 unit tests pass on the branch

Not verifiable without a Render account: live blueprint parsing on Render's side. Everything checkable locally has been checked.

🤖 Generated with Claude Code

adamXbot and others added 2 commits August 5, 2026 02:20
The blueprint could not have provisioned as written, and would have
silently lost client IPs if it had.

Blocking: the database specified `plan: starter`, which Render now
classes as a LEGACY instance type that cannot be used for new databases.
Switched to `basic-256mb` (the current flexible type).

Blocking: the database had no `region`, so it defaulted independently of
the web service. Render services reach a database over the private
network only when they share a region — and since `ipAllowList: []`
disables public ingress, a region mismatch leaves the app with no route
to Postgres at all. Both are now pinned to oregon with a comment saying
they must match.

Silent: no TRUST_PROXY_HEADERS, the same defect just fixed for Fly. The
image sets NODE_ENV=production, where mantis distrusts forwarding
headers unless told otherwise, so every hit records ip = null. Also pins
TRUSTED_IP_HEADER=x-forwarded-for, since Render does not strip an
inbound CF-Connecting-IP and mantis tries that header first — without
the pin a client can forge its recorded IP with one header.

Also:
- MANTIS_API_KEY_PEPPER moves from `sync: false` to `generateValue: true`
  — Render generates it once at create and never rotates it on later
  deploys, which matches the write-once requirement exactly and means
  the operator never has to handle the value
- postgresMajorVersion 16 -> "17" (quoted per spec); the schema only
  needs gen_random_uuid(), core since 13
- documents that the first-boot log prints the admin key, and that the
  structured line above the banner carries only the 18-char prefix, so a
  naive grep for mantis_live_ yields a truncated key that 401s
- optional retention/SMTP/BOOTSTRAP_API_KEY blocks, and a sharper
  free-tier warning: a tripwire is idle by definition, so spin-down
  means nearly every REAL hit pays the cold start

Verified by booting the production image against Postgres 17.10 with
exactly this env set: migrations applied, health db:ok, the bootstrap
banner printed a working key (200 with it, 401 without), and a trigger
carrying `X-Forwarded-For: 198.51.100.42` plus a forged
`CF-Connecting-IP: 6.6.6.6` recorded 198.51.100.42.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Findings from a pre-deploy double-check against the current blueprint
spec, on top of the earlier plan/region/proxy fixes:

- diskSizeGB: 5 — omitting it gives Basic-tier databases a 15 GB disk
  by default, ~3x the storage cost for nothing on a canary DB. Disks
  can grow later but never shrink, so pin it small up front.
- postgresMajorVersion "17" -> "18": parity with docker-compose, CI and
  the tier-2 suite, which all run postgres:18. Re-verified the image
  end-to-end against 18.4: migrations applied, health db:ok, bootstrap
  banner key authenticates (200/401), forged CF-Connecting-IP ignored.
- Document that the <name>.onrender.com namespace is global: plain
  "mantis" is taken, and when the name is taken Render silently serves
  at <name>-<suffix>.onrender.com. PUBLIC_BASE_URL then mints canary
  URLs pointing at ANOTHER customer's app — for a tripwire that means
  handed-out canary URLs trigger someone else's server. Usage now says
  pick a unique name, verify the served URL after first deploy, and fix
  PUBLIC_BASE_URL in this file (the blueprint is the source of truth;
  dashboard edits to synced vars are overwritten on the next sync).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant